{%- macro encode_optional(target, source, depth, encodable) -%}
  {
    jobject optionalValue_{{depth}} = nullptr;
    chip::JniReferences::GetInstance().GetOptionalValue({{source}}, optionalValue_{{depth}});
    if (optionalValue_{{depth}}) {
      auto & definedValue_{{depth}} = {{target}}.Emplace();
      {{ encode_value(
          "definedValue_{}".format(depth),
          "optionalValue_{}".format(depth),
          depth+1,
          encodable.without_optional()
      )}}
    }
  }
{%- endmacro %}

{%- macro encode_nullable(target, source, depth, encodable) -%}
  if ({{source}} == nullptr) {
    {{target}}.SetNull();
  } else {
    auto & nonNullValue_{{depth}} = {{target}}.SetNonNull();
    {{encode_value("nonNullValue_{}".format(depth), source, depth+1, encodable.without_nullable())}}
  }
{%- endmacro %}

{%- macro encode_list(target, source, depth, encodable) -%}
  {
    using ListType_{{depth}} = std::remove_reference_t<decltype({{target}})>;
    using ListMemberType_{{depth}} = ListMemberTypeGetter<ListType_{{depth}}>::Type;
    jint {{source}}Size;
    chip::JniReferences::GetInstance().GetListSize({{source}}, {{source}}Size);
    if ({{source}}Size != 0) {
      auto * listHolder_{{depth}} = new ListHolder<ListMemberType_{{depth}}>({{source}}Size);
      listFreer.add(listHolder_{{depth}});

      for (size_t i_{{depth}} = 0; i_{{depth}} < static_cast<size_t>({{source}}Size); ++i_{{depth}}) {
        jobject element_{{depth}};
        chip::JniReferences::GetInstance().GetListItem({{source}}, i_{{depth}}, element_{{depth}});
        {{encode_value(
           "listHolder_{}->mList[i_{}]".format(depth, depth),
           "element_{}".format(depth),
           depth+1, encodable.without_list()
        )}}
      }
      {{target}} = ListType_{{depth}}(listHolder_{{depth}}->mList, {{source}}Size);
    } else {
      {{target}} = ListType_{{depth}}();
    }
  }
{%- endmacro %}

{%- macro encode_value(target, source, depth, encodable) -%}
    {%- if encodable.is_optional -%}
        {{encode_optional(target, source, depth, encodable)}}
    {%- elif encodable.is_nullable -%}
        {{encode_nullable(target, source, depth, encodable)}}
    {%- elif encodable.is_list -%}
        {{encode_list(target, source, depth, encodable)}}
    {%- elif encodable.is_octet_string -%}
        cleanupByteArrays.push_back(chip::Platform::MakeUnique<chip::JniByteArray>(env, static_cast<jbyteArray>({{source}})));
        {{target}} = cleanupByteArrays.back()->byteSpan();
    {%- elif encodable.is_char_string -%}
        cleanupStrings.push_back(chip::Platform::MakeUnique<chip::JniUtfString>(env, static_cast<jstring>({{source}})));
        {{target}} = cleanupStrings.back()->charSpan();
    {%- elif encodable.is_struct -%}
        {% set struct = encodable.get_underlying_struct()  -%}
        {%   for field in struct.fields %}
          {% set fieldEncodable = field | asEncodable(encodable.context) -%}
          jobject {{source}}_{{field.name}}Item_{{depth}};
          chip::JniReferences::GetInstance().GetObjectField({{source}}, "{{field.name}}", "{{fieldEncodable.boxed_java_signature}}", {{source}}_{{field.name}}Item_{{depth}});
          {{ encode_value(
              "{}.{}".format(target, field.name | lowercaseFirst),
              "{}_{}Item_{}".format(source, field.name, depth),
              depth + 1,
              fieldEncodable
          )}}
        {%-   endfor -%}
    {%- elif encodable.is_enum -%}
        {{target}} = static_cast<std::remove_reference_t<decltype({{target}})>>(chip::JniReferences::GetInstance().IntegerToPrimitive({{source}}));
    {%- elif encodable.is_bitmap -%}
        {{target}} = static_cast<std::remove_reference_t<decltype({{target}})>>(chip::JniReferences::GetInstance().{{encodable.boxed_java_type}}ToPrimitive({{source}}));
    {% else -%}
        {{target}} = static_cast<std::remove_reference_t<decltype({{target}})>>(chip::JniReferences::GetInstance().{{encodable.boxed_java_type}}ToPrimitive({{source}}));
    {% endif -%}
{% endmacro -%}

#include <controller/java/zap-generated/CHIPCallbackTypes.h>
#include <controller/java/zap-generated/CHIPInvokeCallbacks.h>
#include <controller/java/zap-generated/CHIPReadCallbacks.h>

#include <app-common/zap-generated/cluster-objects.h>
#include <zap-generated/CHIPClientCallbacks.h>
#include <zap-generated/CHIPClusters.h>

#include <controller/java/AndroidCallbacks.h>
#include <controller/java/AndroidClusterExceptions.h>
#include <controller/java/CHIPDefaultCallbacks.h>
#include <jni.h>
#include <lib/support/CHIPListUtils.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/JniReferences.h>
#include <lib/support/JniTypeWrappers.h>
#include <lib/support/Span.h>
#include <platform/PlatformManager.h>
#include <vector>

#define JNI_METHOD(RETURN, CLASS_NAME, METHOD_NAME)                                                                                \
    extern "C" JNIEXPORT RETURN JNICALL Java_chip_devicecontroller_ChipClusters_00024##CLASS_NAME##_##METHOD_NAME

using namespace chip;
using namespace chip::Controller;

JNI_METHOD(jlong, {{cluster.name | capitalcase}}Cluster, initWithDevice)(JNIEnv * env, jobject self, jlong devicePtr, jint endpointId)
{
    chip::DeviceLayer::StackLock lock;
    DeviceProxy * device = reinterpret_cast<DeviceProxy *>(devicePtr);
    {{cluster.name |  capitalcase}}Cluster * cppCluster = new {{cluster.name | capitalcase}}Cluster(*device->GetExchangeManager(), device->GetSecureSession().Value(), endpointId);
    return reinterpret_cast<jlong>(cppCluster);
}

{% for command in cluster.commands -%}

JNI_METHOD(void, {{cluster.name | capitalcase}}Cluster, 
  {{command.name | lowercaseFirst}})(JNIEnv * env, jobject self, jlong clusterPtr, jobject callback,
  {%- if command.input_param -%}
  {%-    for field in (cluster.structs | named(command.input_param)).fields -%}
    {{ field | toBoxedJavaType }} {{field.name}},
  {%-    endfor  -%}
  {%- endif -%}
    jobject timedInvokeTimeoutMs)
{
    chip::DeviceLayer::StackLock lock;
    CHIP_ERROR err = CHIP_NO_ERROR;
    {{cluster.name | capitalcase}}Cluster * cppCluster;
    
    ListFreer listFreer;
    chip::app::Clusters::{{cluster.name | capitalcase}}::Commands::{{command.name | capitalcase}}::Type request;

    std::vector<Platform::UniquePtr<JniByteArray>> cleanupByteArrays;
    std::vector<Platform::UniquePtr<JniUtfString>> cleanupStrings;
  {%- if command.input_param -%}
  {%-   for field in (cluster.structs | named(command.input_param)).fields -%}
    {{ encode_value(
        "request." + (field.name | lowercaseFirst),
        (field.name | lowercaseFirst), 
        0, 
        field | asEncodable(typeLookup)
    )}}
  {%-   endfor -%}
  {% endif %}

  {% set callbackName = command | commandCallbackName(cluster) %}
    std::unique_ptr<CHIP{{callbackName}}Callback, void (*)(CHIP{{callbackName}}Callback *)> onSuccess(
        Platform::New<CHIP{{callbackName}}Callback>(callback), Platform::Delete<CHIP{{callbackName}}Callback>);
    std::unique_ptr<CHIPDefaultFailureCallback, void (*)(CHIPDefaultFailureCallback *)> onFailure(Platform::New<CHIPDefaultFailureCallback>(callback), Platform::Delete<CHIPDefaultFailureCallback>);
    VerifyOrReturn(onSuccess.get() != nullptr, AndroidClusterExceptions::GetInstance().ReturnIllegalStateException(env, callback, "Error creating native callback", CHIP_ERROR_NO_MEMORY));
    VerifyOrReturn(onFailure.get() != nullptr, AndroidClusterExceptions::GetInstance().ReturnIllegalStateException(env, callback, "Error creating native callback", CHIP_ERROR_NO_MEMORY));

    cppCluster = reinterpret_cast<{{cluster.name | capitalcase}}Cluster *>(clusterPtr);
    VerifyOrReturn(cppCluster != nullptr, AndroidClusterExceptions::GetInstance().ReturnIllegalStateException(env, callback, "Error getting native cluster", CHIP_ERROR_INCORRECT_STATE));

    auto successFn = chip::Callback::Callback<CHIP{{callbackName}}CallbackType>::FromCancelable(onSuccess->Cancel());
    auto failureFn = chip::Callback::Callback<CHIPDefaultFailureCallbackType>::FromCancelable(onFailure->Cancel());

  {% if command.is_timed_invoke -%}
    err = cppCluster->InvokeCommand(request, onSuccess->mContext, successFn->mCall, failureFn->mCall, chip::JniReferences::GetInstance().IntegerToPrimitive(timedInvokeTimeoutMs));
  {%- else -%}
    if (timedInvokeTimeoutMs == nullptr) {
        err = cppCluster->InvokeCommand(request, onSuccess->mContext, successFn->mCall, failureFn->mCall);
    } else {
        err = cppCluster->InvokeCommand(request, onSuccess->mContext, successFn->mCall, failureFn->mCall, chip::JniReferences::GetInstance().IntegerToPrimitive(timedInvokeTimeoutMs));
    }
  {%- endif %}
    VerifyOrReturn(err == CHIP_NO_ERROR, AndroidClusterExceptions::GetInstance().ReturnIllegalStateException(env, callback, "Error invoking command", CHIP_ERROR_INCORRECT_STATE));

    onSuccess.release();
    onFailure.release();
}
{% endfor %}

{%- for attr in cluster.attributes if attr.is_subscribable -%}
{%-   if attr | canGenerateSubscribe(typeLookup) -%}

JNI_METHOD(void, {{cluster.name}}Cluster, subscribe{{attr.definition.name | capitalcase}}Attribute)(JNIEnv * env, jobject self, jlong clusterPtr, jobject callback, jint minInterval, jint maxInterval)
{
    chip::DeviceLayer::StackLock lock;

    {%- set callbackName = attr | callbackName(cluster, typeLookup) -%}

    std::unique_ptr<{{callbackName}}, void (*)({{callbackName}} *)> onSuccess(Platform::New<{{callbackName}}>(callback, true), chip::Platform::Delete<{{callbackName}}>);
    VerifyOrReturn(onSuccess.get() != nullptr, chip::AndroidClusterExceptions::GetInstance().ReturnIllegalStateException(env, callback, "Error creating native success callback", CHIP_ERROR_NO_MEMORY));

    std::unique_ptr<CHIPDefaultFailureCallback, void (*)(CHIPDefaultFailureCallback *)> onFailure(Platform::New<CHIPDefaultFailureCallback>(callback), chip::Platform::Delete<CHIPDefaultFailureCallback>);
    VerifyOrReturn(onFailure.get() != nullptr, chip::AndroidClusterExceptions::GetInstance().ReturnIllegalStateException(env, callback, "Error creating native failure callback", CHIP_ERROR_NO_MEMORY));

    CHIP_ERROR err = CHIP_NO_ERROR;
    {{cluster.name}}Cluster * cppCluster = reinterpret_cast<{{cluster.name}}Cluster *>(clusterPtr);
    VerifyOrReturn(cppCluster != nullptr, chip::AndroidClusterExceptions::GetInstance().ReturnIllegalStateException(env, callback, "Could not get native cluster", CHIP_ERROR_INCORRECT_STATE));

    using TypeInfo = chip::app::Clusters::{{cluster.name}}::Attributes::{{attr.definition.name | capitalcase}}::TypeInfo;
    auto successFn = chip::Callback::Callback<CHIP{{cluster.name}}Cluster{{attr.definition.name | capitalcase}}AttributeCallbackType>::FromCancelable(onSuccess->Cancel());
    auto failureFn = chip::Callback::Callback<CHIPDefaultFailureCallbackType>::FromCancelable(onFailure->Cancel());

    err = cppCluster->SubscribeAttribute<TypeInfo>(onSuccess->mContext, successFn->mCall, failureFn->mCall, static_cast<uint16_t>(minInterval), static_cast<uint16_t>(maxInterval), {{callbackName}}::OnSubscriptionEstablished);
    VerifyOrReturn(err == CHIP_NO_ERROR, chip::AndroidClusterExceptions::GetInstance().ReturnIllegalStateException(env, callback, "Error subscribing to attribute", err));

    onSuccess.release();
    onFailure.release();
}

{%-   endif -%}
{% endfor %}
